在做專案時, 都會需要關聯式資料庫做資料的CRUD.
Go提供了database/sql包來讓開發者跟資料庫打交道, 這包就像Java的JDBC.
database/sql包只是定義了一套操作資料庫的接口和抽象層定義.
所以還是需要實體的驅動, 這裡我選用MySQL.
各種Go SQL Drivers
我們開發者幾乎都是在操作database/sql包所提供的接口方法而已.
大部分情境, 都只要在程式的某地方設定好驅動就好.
go get github.com/go-sql-driver/mysql
來認識幾個type, 因為我在這撞牆蠻久的.
C#跟Node套件用習慣了QQ
資料庫的物件實例, 同時內部也有自己實現的連線池, 其池是一個包含多個open和idle連線的池子.
使用時被選到的連線會被標記成open
, 完成後會被標記成idle
他需要有driver打開或關閉資料庫, 管理連線池.
同時它也是線程安全的, 可以不必重複創立, 只需要一個就可以傳遞給多個goroutine使用.
一個泳池的泳道數量設定好之後就是固定的, 只要有人使用, 該泳道就被open, 等到有人離開泳道, 該泳道就被視為idel.
maxlifetime, 就視為該泳道的開放使用時間吧, 也許設置成1小時換水清理一次.
但有人還在用的話, 當然就要等它被釋放出來, 才能開始清潔. (有點牽強的例子)
設定空閒連線池的最大連線數
func (db *DB) SetMaxIdleConns(n int) {...}
設定最大打開的連線數, 0表示不限制.
func(db * DB)SetMaxOpenConns(n int) {...}
設定連線可重複利用的最大時間長度, 0是預設值表示沒有max life, 總是可重複使用.
func (db *DB) SetConnMaxLifetime(d time.Duration) {...}
db.SetConnMaxLifetime(time.Hour)
// 設定每一個連線最大生命週期1hr
//
func Open(driverName, dataSourceName string) (*DB, error) {...}
初始化一個sql.DB對象, 但還沒真正建立連線.
會啟動一個connectionOpener的goroutine.
也初始化一個openerCh channel.
需要連線時, 就對該channel發送數據就好.
db := &DB{
driver: driveri,
dsn: dataSourceName,
openerCh: make(chan struct{}, connectionRequestQueueSize),
lastPut: make(map[*driverConn]string),
}
在這兩種情形下會去建立連線 :
go-sql-driver/mysql所提供的結構體, 有提供幾個針對DSN的方法
// config的建構式
func NewConfig() *Config
// 把dsn字串剖析轉成config物件
func ParseDSN(dsn string) (cfg *Config, err error)
// 複製一個config物件的副本
func (cfg *Config) Clone() *Config
// 把config結構體格式化成dsn格式的連線字串
func (cfg *Config) FormatDSN() string
呼叫QueryRow()之後返回的單行結果
掃描取得的結果到dest上; 但如果有多個結果, scan做完第一行後, 就會丟棄其他行的資料了.
如果row是沒有資料的, 則是會返回ErrNoRows這錯誤.
func (r *Row) Scan(dest ...interface{}) error {...}
查詢的結果, 會有個指標從開始直到結束.
呼叫Next(), 就去取得下一行row的資料.
// 主要是用來讓連線釋放回連線池
func (rs *Rows) Close() error {...}
// 對資料做走訪,正常結束的話內部會自動呼叫rows.Close()
func (rs *Rows) Next() bool
// 切換到下一個結果集, 一次查詢是可以返回多個結果集的
func (rs *Rows) NextResultSet() bool
//
func (rs *Rows) Scan(dest ...interface{}) error
type Scanner interface {
// Scan assigns a value from a database driver.
Scan(src interface{}) error
}
查詢語句, DDL、DML等的prepared sql語句.
一個進行中的資料庫事務.
呼叫db.Begin()之後, 會取得一個tx物件, 需要呼叫Commit()或Rollback()才會結束事務.並且歸還連線回連線池.
主要是針對insert、update、delete的操作所返回的結果.
Result是個接口, 定義了LastInsertId()和RowsAffected().
各驅動會實作這接口的兩個方法.
type Result interface {
LastInsertId() (int64, error)
RowsAffected() (int64, error)
}
MySqlDriver的實作:
package mysql
type mysqlResult struct {
affectedRows int64
insertId int64
}
func (res *mysqlResult) LastInsertId() (int64, error) {
return res.insertId, nil
}
func (res *mysqlResult) RowsAffected() (int64, error) {
return res.affectedRows, nil
}
各種允許null的基礎型別. 且都有實現Scanner的Scan().
事務用的資料隔離等級.
const(
LevelDefault IsolationLevel = iota
LevelReadUncommitted
LevelReadCommitted
LevelWriteCommitted
LevelRepeatableRead
LevelSnapshot
LevelSerializable
LevelLinearizable
)
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"fmt"
)
func main() {
}
就這樣...
疑? 別懷疑!!!
我們來看看sql包提供什麼方法給mysql注入驅動
mysql又怎麼偷偷去注入的.
database/sql包的Register(), 有提供該方法注入驅動名稱跟驅動物件實例
func Register(name string, driver driver.Driver)
go-sql-driver/mysql則是在driver.go這裡有註冊了init要執行的動作.
// go-sql-driver/mysql/blob/master/driver.go
package mysql
import (
"database/sql"
"database/sql/driver"
)
func init() {
sql.Register("mysql", &MySQLDriver{})
}
還記得之前提到的package在相互import時, 如果有init會呼叫吧!
就是在這裡偷偷的注入了驅動.
有了驅動, 就來建立連線.
package main
import (
"database/sql"
"fmt"
"github.com/go-sql-driver/mysql"
)
func main() {
config := mysql.Config{
User: "root",
Passwd: "m_root_pwd",
Addr: "172.31.0.11:3306",
Net: "tcp",
DBName: "testSync",
AllowNativePasswords: true,
}
fmt.Println("conn: ", config.FormatDSN())
// db, err := sql.Open("mysql", "root:m_root_pwd@tcp(172.31.0.11:3306)/testSync")
// Open()並不會真的去連接DB
db, err := sql.Open("mysql", config.FormatDSN())
if err != nil {
fmt.Println(err)
}
// 釋放連線
defer db.Close()
// Ping會真的建立一條連線
err = db.Ping()
if err != nil {
fmt.Println(err)
}
}
/*
conn: root:m_root_pwd@tcp(172.31.0.11:3306)/testSync?maxAllowedPacket=0
*/
利用中斷點可以明顯的知道Open()並無真正建立連線.
DB位置亂打也不會真的給錯誤XD.
這裡的連線字串是採用DSN(Data Source Name)的結構
[username[:password]@][protocol[(host:[port])]]/dbname[?param1=value1&...¶mN=valueN]
config.FormatDSN()則是把config轉成DSN格式的字串
重要提醒
DB的連線都是被設計來當作長連線使用的, 所以不該頻繁的Open、Close.
Open()也並不是真正建立連線, 也沒去驗證連線參數, 只是提早準備好資料庫的抽象實例, 方便等等使用.
Open取得的db實例, 要重複一直利用, 不應該去重複生成.
但若程式退出, 最好在主執行緒上執行db.Close().
不然就要等MySQL主動來確認這連線是否還健康.
package main
import (
"database/sql"
"fmt"
"log"
"github.com/go-sql-driver/mysql"
)
const (
CreateUsersTable = "CREATE TABLE IF NOT EXISTS `users` (`user_id` bigint(20) NOT NULL AUTO_INCREMENT, `user_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, `created_time` timestamp NOT NULL DEFAULT current_timestamp, `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
)
func logIfErr(err error) {
if err != nil {
log.Printf("error occurred: %s", err)
}
}
func main() {
config := mysql.Config{
User: "root",
Passwd: "m_root_pwd",
Addr: "172.31.0.11:3306",
Net: "tcp",
DBName: "testSync",
AllowNativePasswords: true,
}
fmt.Println("conn: ", config.FormatDSN())
// db, err := sql.Open("mysql", "root:m_root_pwd@tcp(172.31.0.11:3306)/testSync")
db, err := sql.Open("mysql", config.FormatDSN())
logIfErr(err)
defer db.Close()
// create users table
_, err = db.Exec(CreateUsersTable)
logIfErr(err)
// insert data
result, err := db.Exec("INSERT INTO `users` (`user_name`) VALUES('test02')")
logIfErr(err)
rowCount, err := result.RowsAffected()
logIfErr(err)
log.Print(rowCount)
// query data
rows, err := db.Query("SELECT user_name FROM users")
logIfErr(err)
for rows.Next() {
var s string
err = rows.Scan(&s)
logIfErr(err)
log.Print("%q", s)
}
rows.Close()
// query data with arguments
rows, err = db.Query("SELECT user_name FROM users WHERE user_name = ?", "nathan")
logIfErr(err)
for rows.Next() {
var s string
err = rows.Scan(&s)
logIfErr(err)
log.Print("%q", s)
}
rows.Close()
}
資料庫會接收到各種不同的敘述來執行. 尤其就以查詢來說, 很可能內容都是一樣的, 只是where條件稍微不同. 但是每一次接收到敘述時都還是要 檢查->解析->執行->回傳
這樣的一套流程.
要是我們可以省下檢查->解析的過程, 每次只要把變數代入做 執行->回傳的動作的話.
速度上會快上一些.
SELECT user_name FROM users WHERE user_name = ? ;
這個?我們叫做佔位符號(placeholders); 用來避免直接在sql作字串拼接, 可以防止大部分的sql injection
MySQL | Postgres | Oracle |
---|---|---|
WHERE col = ? | WHERE col = $1 | WHERE col = :col |
VALUES(?, ?, ?) | VALUES($1, $2, $3) | VALUES(:val1, :val2, :val3) |
預處理過得語句在資料庫中會被存起來, 資料庫會對該語句先作檢查->剖析, 作執行計畫的判斷跟語句優化.
而後我們只要帶入變數資料, 就能直接執行了.
userName := "nathan"
stmt, err := db.Prepare("SELECT user_name FROM users WHERE user_name = ? ;")
if err != nil {
log.Fatal(err)
}
rows, err := stmt.Query(userName)
defer stmt.Close()
stmt,_ := db.Prepare("SELECT uid,username FROM USER WHERE age = ?")
defer stmt.Close()
// 同樣語句, 不同變數; 這樣連線也只要跟連線池要一次就好
for age := 18; age < 100 ; age ++ {
rows,_ = stmt.Query(age)
defer rows.Close()
for rows.Next(){
var name string
var id int
if err := rows.Scan(&id,&name); err != nil {
log.Fatal(err)
}
}
}
stmt,_ := db.Prepare("SELECT uid,username FROM USER WHERE age = ?")
defer stmt.Close()
// 同樣語句, 不同變數; 這樣連線也只要跟連線池要一次就好
for outerAge := 18; outerAge < 100; outerAge++ {
go func(age int) {
rows, _ = stmt.Query(age)
defer rows.Close()
for rows.Next() {
var name string
var id int
if err := rows.Scan(&id, &name); err != nil {
log.Fatal(err)
}
}
}(outerAge)
}
Tx幾乎都是為了先鎖定確認資料的值, 才決定執行異動.
或者是多筆異動都要確保一起完成或失敗.
透過Next()得知還有沒有下一筆資料.
這時就能思考是否這時Query成功後, 其實所有的資料都在Go的服務的記憶中了??
其實不是XD 真要是這樣那些撈報表的早就記憶體被塞爆了.
也沒有必要去手動遞延呼叫rows.Close(), 來歸還連線.
之間的溝通全靠COM_QUERY在串流式的發送命令和讀取TCP package進到buffer.
直到收到EOF時, 就表示沒有下一筆了.
readRow()原始程式
我以後再補一篇在個人網誌, 用wireshark就能抓到這mysql數據包了
明天會介紹更多DB用法, 應該吧.
資料庫許多事情, 能到BackendTw臉書社團詢問T大, 他的經驗跟講座都很值得聽.